package NEHE.JOGL.Port;

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.media.opengl.*;
import javax.media.opengl.glu.GLU;
import javax.swing.*;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.texture.*;
import static javax.media.opengl.GL.*;
import static java.awt.event.KeyEvent.*;

/**
 * NeHe Lesson 9: Moving Bitmaps In 3D Space
 */
public class NeheJOGL09Stars extends JPanel implements GLEventListener, KeyListener {
   private static final int REFRESH_FPS = 60;    // Display refresh frames per second
   private GLU glu;             // For the GL Utility
   final FPSAnimator animator;  // Used to drive display() 
   GLCanvas canvas;
   
   // Twinkling stars
   private static boolean twinkleOn = true; // twinkle on/off

   public static final int numStars = 50; // number of twinkling stars
   private Star[] stars = new Star[numStars]; // array of Star objects

   private static float z = -15.0f; // viewing distance away from stars
   private static float tilt = 90.0f; // tilting angle of the view
   private static float starSpinAngle = 0.0f; // spin stars on the z-axis
   private static float zIncrement = 1.0f; // increment per key pressed
   private static float tileIncrement = 1.0f;

   // Texture applied over the shape
   private Texture texture; // texture object
   private String textureFileName = "images/star.bmp";

   // Texture image flips vertically. Shall use TextureCoords class to retrieve the
   // top, bottom, left and right coordinates.
   private float textureCoordTop;
   private float textureCoordBottom;
   private float textureCoordLeft;
   private float textureCoordRight;

   // Constructor
   public NeheJOGL09Stars() {
      canvas = new GLCanvas();
      this.setLayout(new BorderLayout());
      this.add(canvas, BorderLayout.CENTER);
      canvas.addGLEventListener(this);
      canvas.addKeyListener(this);
      canvas.setFocusable(true);  // To receive key event
      canvas.requestFocus();
   
      // Run the animation loop using the fixed-rate Frame-per-second animator,
      // which calls back display() at this fixed-rate (FPS).
      animator = new FPSAnimator(canvas, REFRESH_FPS, true);
   }

   // Main program
   public static void main(String[] args) {
      final int WINDOW_WIDTH = 320;
      final int WINDOW_HEIGHT = 240;
      final String WINDOW_TITLE = "Nehe #9: Moving Bitmaps in 3D Space";

      JFrame frame = new JFrame();
      final NeheJOGL09Stars joglMain = new NeheJOGL09Stars();
      frame.setContentPane(joglMain);
      frame.addWindowListener(new WindowAdapter() {
         @Override 
         public void windowClosing(WindowEvent e) {
            // Use a dedicate thread to run the stop() to ensure that the
            // animator stops before program exits.
            new Thread() {
               @Override 
               public void run() {
                  joglMain.animator.stop(); // stop the animator loop
                  System.exit(0);
               }
            }.start();
         }
      });
      frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
      frame.setTitle(WINDOW_TITLE);
      frame.setVisible(true);
      joglMain.animator.start(); // start the animation loop
   }

   // ------ Implement methods declared in GLEventListener ------

   /**
    * Called by the drawable immediately after the OpenGL context is initialized for
    * the first time. Can be used to perform one-time OpenGL initialization such as
    * setup of lights and display lists. Run only once.
    */
   @Override
   public void init(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // GL Utilities
      glu = new GLU();
      // Enable smooth shading, which blends colors nicely across a polygon, and
      // smoothes out lighting.
      gl.glShadeModel(GL_SMOOTH);
      // Set background color (in RGBA). Alpha of 0 for total transparency
      gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
      // Setup the depth buffer & enable the depth testing
      gl.glClearDepth(1.0f);
      // No depth testing for this lesson
      // gl.glEnable(GL_DEPTH_TEST); // enables depth testing
      // gl.glDepthFunc(GL_LEQUAL); // the type of depth test to do
      // We want the best perspective correction to be done
      gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
      
      // Load the texture image
      try {
         // Use URL so that can read from JAR and disk file.
         BufferedImage image = ImageIO.read(this.getClass().getResource(textureFileName));

         // Create a OpenGL Texture object from (URL, mipmap, file suffix)
         texture = TextureIO.newTexture(image, false);
         // Use linear filter if image is larger than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
         // Use linear filter if image is smaller than the original texture
         gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

      } catch (GLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      // Texture image flips vertically. Shall use TextureCoords class to retrieve
      // the top, bottom, left and right coordinates, instead of using 0.0f and 1.0f.
      TextureCoords textureCoords = texture.getImageTexCoords();
      textureCoordTop = textureCoords.top();
      textureCoordBottom = textureCoords.bottom();
      textureCoordLeft = textureCoords.left();
      textureCoordRight = textureCoords.right();

      // Enable the texture
      texture.enable();
      texture.bind();
      // gl.glEnable(GL_TEXTURE_2D);
      
      // Enable blending
      gl.glEnable(GL_BLEND);
      gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE);

      // Allocate the stars
      for (int i = 0; i < stars.length; i++) {
         stars[i] = new Star();
         // Linearly distributed according to the star number
         stars[i].distance = ((float)i / numStars) * 5.0f;
      }
   }

   /**
    * Called by the drawable to initiate OpenGL rendering.
    */
   @Override
   public void display(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // Clear the screen and the depth buffer
      gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      
      for (int i = 0; i < stars.length; i++) {
         // Reset the view (x, y, z axes back to normal)
         gl.glLoadIdentity();
         gl.glTranslatef(0.0f, 0.0f, z);

         // The stars are texture quad square drawn on x-y plane and
         // distributed on on x-z plane around the y-axis

         // Initial 90 degree tile in the x-axis, y-axis pointing out of screen
         gl.glRotatef(tilt, 1.0f, 0.0f, 0.0f);
         // Rotate about y-axis (pointing out of screen), initial angle is 0
         gl.glRotatef(stars[i].angle, 0.0f, 1.0f, 0.0f);
         // Translate about the x-axis (pointing right) to its current distance
         gl.glTranslatef(stars[i].distance, 0.0f, 0.0f);

         // The stars have initial angle of 0, and initial distance linearly
         // distributed between 0 and 5.0f

         // Rotate the axes back, so that z-axis is again facing us, to ensure that
         // the quad (with texture) on x-y plane is facing us
         gl.glRotatef(-stars[i].angle, 0.0f, 1.0f, 0.0f);
         gl.glRotatef(-tilt, 1.0f, 0.0f, 0.0f);

         // Take note that without the two rotations and undo, there is only one
         // translation along the x-axis
         // Matrix operation is non-commutative. That is, AB != BA.
         // Hence, ABCB'A' != C

         // Draw the star, which spins on the z axis (pointing out of the screen)
         gl.glRotatef(starSpinAngle, 0.0f, 0.0f, 1.0f);
         // Set the star's color using bytes (why bytes? not float or int?)
         gl.glColor4ub(stars[i].r, stars[i].g, stars[i].b, (byte)255);
         gl.glBegin(GL_QUADS);
         // draw a square on x-y plane
         gl.glTexCoord2f(textureCoordLeft, textureCoordBottom);
         gl.glVertex3f(-1.0f, -1.0f, 0.0f);
         gl.glTexCoord2f(textureCoordRight, textureCoordBottom);
         gl.glVertex3f(1.0f, -1.0f, 0.0f);
         gl.glTexCoord2f(textureCoordRight, textureCoordTop);
         gl.glVertex3f(1.0f, 1.0f, 0.0f);
         gl.glTexCoord2f(textureCoordLeft, textureCoordTop);
         gl.glVertex3f(-1.0f, 1.0f, 0.0f);
         gl.glEnd();

         // If twinkling, overlay with another drawing of an arbitrary color
         if (twinkleOn) {
            // Assign a color using bytes
            gl.glColor4ub(stars[(numStars - i) - 1].r, stars[(numStars - i) - 1].g,
                  stars[(numStars - i) - 1].b, (byte)255);
            gl.glBegin(GL_QUADS);
            // draw a square on x-y plane
            gl.glTexCoord2f(textureCoordLeft, textureCoordBottom);
            gl.glVertex3f(-1.0f, -1.0f, 0.0f);
            gl.glTexCoord2f(textureCoordRight, textureCoordBottom);
            gl.glVertex3f(1.0f, -1.0f, 0.0f);
            gl.glTexCoord2f(textureCoordRight, textureCoordTop);
            gl.glVertex3f(1.0f, 1.0f, 0.0f);
            gl.glTexCoord2f(textureCoordLeft, textureCoordTop);
            gl.glVertex3f(-1.0f, 1.0f, 0.0f);
            gl.glEnd();
         }

         // Update for the next refresh
         // The star spins about the z-axis (pointing out of the screen), and spiral
         // inwards and collapse towards the center, by increasing the angle on x-y
         // plane and reducing the distance.

         starSpinAngle += 0.01f; // used to spin the stars about the z-axis
         // spiral pattern
         stars[i].angle += (float)i / numStars; // changes the angle of a star
         // collapsing the star to the center
         stars[i].distance -= 0.01f; // changes the distance of a star
         // re-bone at the edge
         if (stars[i].distance < 0.0f) { // Is the star collapsed to the center?
            stars[i].distance += 5.0f; // move to the outer ring
            stars[i].setRandomRGB(); // choose a random color for the star
         }
      }
   }

   /**
    * Called by the drawable during the first repaint after the component has been
    * resized. Run at least once when the window is first set to visible.
    */
   @Override
   public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();

      if (height == 0) {
         height = 1; // prevent divide by zero
      }
      float aspect = (float)width / height;

      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);

      // Enable perspective view (where far objects are smaller)
      gl.glMatrixMode(GL_PROJECTION);
      gl.glLoadIdentity(); // reset
      // View angle of 45 degrees, based on this canvas's aspect ratio. The 0.1f,
      // 100.0f is the starting and ending points for how deep we can draw into
      // the screen.
      glu.gluPerspective(45.0f, aspect, 0.1f, 100.0f); // fovy, aspect, zNear, zFar

      // Enable the model view - any new transformations will affect the model-view
      // matrix
      gl.glMatrixMode(GL_MODELVIEW);
      gl.glLoadIdentity(); // reset
   }

   /**
    * Called when the display mode (eg. resolution) has been changed.
    */
   @Override
   public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
         boolean deviceChanged) {}

   // ----- Implement methods declared in KeyListener -----

   @Override
   public void keyPressed(KeyEvent e) {
      switch (e.getKeyCode()) {
         case VK_T: // toggle twinkle
            twinkleOn = !twinkleOn;
            break;
         case VK_UP: // decrease the tile (rotation bout x-axis)
            tilt -= tileIncrement;
            break;
         case VK_DOWN: // increase the tile (rotation bout x-axis)
            tilt += tileIncrement;
            break;
         case VK_PAGE_UP: // zoom out (further z)
            z -= zIncrement;
            break;
         case VK_PAGE_DOWN: // zoom in (nearer z)
            z += zIncrement;
            break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {}

   @Override
   public void keyTyped(KeyEvent e) {
      switch (e.getKeyChar()) {

      }
   }

   // Star class (inner class)
   class Star {
      // public access for simplicity
      public byte r, g, b; // RGB values for the star
      public float distance; // distance from the center
      public float angle; // current angle about the center

      private Random rand = new Random();

      // Constructor
      public Star() {
         angle = 0.0f;
         r = (byte)rand.nextInt(256);
         g = (byte)rand.nextInt(256);
         b = (byte)rand.nextInt(256);
      }

      // Set the RGB color of this star to some random values
      public void setRandomRGB() {
         r = (byte)rand.nextInt(256);
         g = (byte)rand.nextInt(256);
         b = (byte)rand.nextInt(256);
      }
   }
}
